/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, NodePredicate } from 'qvog-dsl';
import { rule } from 'qvog-dsl';

const SEVERITY = 2;
const DOC_PATH = 'docs/typedef.md';
const DESCRIPTION = 'Require type annotations in specific locations';

interface TypedefOptions {
  arrayDestructuring?: boolean;
  arrowParameter?: boolean;
  memberVariableDeclaration?: boolean;
  objectDestructuring?: boolean;
  parameter?: boolean;
  propertyDeclaration?: boolean;
  variableDeclaration?: boolean;
  variableDeclarationIgnoreFunction?: boolean;
}

const DEFAULT_OPTIONS: TypedefOptions = {
  arrayDestructuring: true,
  arrowParameter: true,
  memberVariableDeclaration: true,
  objectDestructuring: true,
  parameter: true,
  propertyDeclaration: true,
  variableDeclaration: true,
  variableDeclarationIgnoreFunction: true,
};


const TypedefNode: NodePredicate<ts.Node> = {
  is(node: ts.Node): node is ts.Node {
    return ts.isVariableDeclaration(node) ||
      ts.isParameter(node) ||
      ts.isPropertyDeclaration(node) ||
      ts.isPropertySignature(node) ||
      (ts.isBinaryExpression(node) && ts.isObjectLiteralExpression(node.left));
  }
};

export default rule<{ ruleOption: TypedefOptions }>()
  .match(TypedefNode)
  .when((node: ts.Node, { props }): boolean => {
    const options: TypedefOptions = { ...DEFAULT_OPTIONS, ...(props.ruleOption ?? {}) };

    if (ts.isVariableDeclaration(node)) {
      return checkVariableDeclaration(node, options);
    }
    if (ts.isParameter(node)) {
      return checkParameter(node, options);
    }

    if (ts.isPropertyDeclaration(node)) {
      return !!options?.memberVariableDeclaration && !node.type;
    }

    if (ts.isPropertySignature(node)) {
      return !!options?.propertyDeclaration && !node.type;
    }

    if (ts.isBinaryExpression(node) && ts.isObjectLiteralExpression(node.left)) {
      return !!options?.objectDestructuring;
    }


    return false;
  })
  .report({
    severity: SEVERITY,
    description: DESCRIPTION,
    docPath: DOC_PATH,
  });

function checkVariableDeclaration(node: ts.VariableDeclaration, options: TypedefOptions): boolean {
  if (node.type) { return false };

  if (ts.isArrayBindingPattern(node.name)) {
    return options.arrayDestructuring ?? false;
  }
  if (ts.isObjectBindingPattern(node.name)) {
    return options.objectDestructuring ?? false;
  }

  const isFunctionInit = node.initializer &&
    (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer));

  if (isFunctionInit) {
    return options.variableDeclarationIgnoreFunction ?? false;
  }
  return options.variableDeclaration ?? false;
}

function checkParameter(node: ts.ParameterDeclaration, options: TypedefOptions): boolean {
  if (node.type) { return false };

  if (ts.isArrayBindingPattern(node.name) || ts.isObjectBindingPattern(node.name)) {
    return options.objectDestructuring ?? false;
  }

  const isArrowParam = ts.isArrowFunction(node.parent);
  return isArrowParam
    ? options.arrowParameter ?? false
    : options.parameter ?? false;
}